네이버 영화 리뷰 데이터셋 EDA

Hodong Lee, 18.04.21


영화 리뷰를 통한 평점 예측을 위한 CNN 모델 설계에 앞서 이 과정을 통해 다음 3가지에 대한 인사이트를 얻는 것을 목표로 한다.

1) FC layer에 2개의 분류기를 직렬로 연결하는 계층적 구조\*의 CNN 모델\*이 의미가 있는지
2) input data를 어떻게 손질할지와 각 방법은 어떤 차이를 보이는지 (cohesion vs. mecab, word vs. bigram)
3) 정보량이 높은 token(혹은 bigram)을 어떻게 추출할지

각 카테고리/점수별로 분포된 단어가 명확한 차이를 보인다면 계층적 구조는 시도할 가치가 있는 것으로 판단할 수 있다. 또, cohesion token과 mecab token을 빈도수와 tf-idf 2가지 기준으로 시각화를 해봄으로써 어떤 토큰을 input data로 사용하며, 현재 토큰 전체를 모두 사용하는 구조에서 정보량이 높은 상위 n개의 토큰을 뽑아내는 방법을 찾아본다.


  • FC layer의 계층적 설계는 [Land use Classification using Convolutional Neural Networks Appled to Ground-Level Images] 논문을 참고했습니다.
  • CNN 모델은 [Convolutional Neural Networks for Sentence Classification] 논문을 참고했습니다.




In [2]:
#for basic data manipuldation
require(stats)
require(plyr)
require(dplyr) 
require(lubridate) #for processing time-series data
require(geosphere)
require(reshape)
require(tibble)
require(stringr)
require(SnowballC)
require(tidytext)
require(tidyr)

#for basic visualization
require(extrafont) #for using 'Helvetica'
require(RColorBrewer)
require(ggplot2) #basic visualization
require(GGally)
require(grid)
In [3]:
#multiplot function
multiplot <- function(..., plotlist = NULL, file, cols = 1, layout = NULL) {
  require(grid)
  plots <- c(list(...), plotlist)
  numPlots = length(plots)
  if (is.null(layout)) {
    layout <- matrix(seq(1, cols * ceiling(numPlots/cols)),
                     ncol = cols, nrow = ceiling(numPlots/cols))}
  if (numPlots == 1) { print(plots[[1]])
  } else {
    grid.newpage()
    pushViewport(viewport(layout = grid.layout(nrow(layout), ncol(layout))))
    
    for (i in 1:numPlots) {
      matchidx <- as.data.frame(which(layout == i, arr.ind = TRUE))
      print(plots[[i]], vp = viewport(layout.pos.row = matchidx$row,
                                      layout.pos.col = matchidx$col)) }}}


In [4]:
setwd("/Users/hodong/Desktop/jupyter_prac/nsml/analysis")
cohesion <- data.frame(read.csv("./data/cohesion_tokens.csv", header=FALSE))
mecab <- data.frame(read.csv("./data/mecab_tokens.csv", header=FALSE))
tag <- data.frame(read.csv("./data/mecab_tokens_tag.csv", header=FALSE))
label <- data.frame(read.csv("./data/labels.csv",  header=FALSE))

names(label) <- c("id", "score")
names(cohesion) <- c("id", "review")
names(mecab) <- c("id", "review")
names(tag) <- c("id", "tag")

cohesion$review <- as.character(cohesion$review)
mecab$review <- as.character(mecab$review)
tag$tag <- as.character(tag$tag)


* data description

  • label : 각 영화 리뷰별의 평점
    • {리뷰id, 영화 평점}
  • cohesion : cohesion tokenizer를 이용해 토큰화한 리뷰. 각 토큰은 " "으로 join되어 하나의 문자열로 변환된 상태.
    • {리뷰id, cohesion_token string}
  • token : mecab 형태소분석기를 이용해 토큰화한 리뷰. 각 토큰은 " "으로 join되어 하나의 문자열로 변환된 상태.
    • {리뷰id, mecab_token string}
  • tag : mecab 형태소분석기를 이용해 토큰화한 뒤 각 토큰의 품사를 " "으로 join해 하나의 문자열로 변환한 상태.
    • {리뷰id, mecab_tag string}
In [5]:
glimpse(label)
Observations: 65,000
Variables: 2
$ id    <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18...
$ score <int> 5, 1, 10, 10, 10, 10, 7, 6, 9, 8, 8, 7, 7, 7, 7, 8, 10, 10, 1...
In [6]:
glimpse(cohesion)
Observations: 65,000
Variables: 2
$ id     <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 1...
$ review <chr> " 재밌 는데 지루함 멋있 는데 지루함 정말 딱 뭣도 아닌 난이 걸 얼마 나 기대했던 가", " 아진짜알바좀...
In [7]:
glimpse(token)
Observations: 65,000
Variables: 2
$ id     <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 1...
$ review <chr> " 재밌 는데 지루 함 멋있 는데 지루 함 정말 딱 뭣 도 아닌 난이 걸 얼마 나 기대 했 던 가", " 아...
In [8]:
glimpse(tag)
Observations: 65,000
Variables: 2
$ id  <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, ...
$ tag <chr> " VA VV+EC XR XSA+ETN VA VV+EC XR XSA+ETN MAG MAG NP JX VCN+ETM...



2. 데이터 분포 및 전처리

데이터 분포를 간단하게 살펴보고, 필요한 정보를 볼 수 있도록 전처리한다.

* 점수에 따라 category labeling

  • 0~3 : bad
  • 4~6 : soso
  • 7~10 : good
In [9]:
bad <- label[which(label$score<4), ]$id
soso <- label[which(label$score>=4 & label$score<=7), ]$id
good <- label[which(label$score>7), ]$id

label <- cbind(label, data.frame(matrix(nrow=nrow(label), ncol=1)))
names(label) <- c("id", "score", "category")


label$score <- as.numeric(label$score)
label[bad, ]$category <- "bad"
label[soso, ]$category <- "soso"
label[good, ]$category <- "good"
In [10]:
print(length(bad))
print(length(soso))
print(length(good))
[1] 8931
[1] 6709
[1] 49360


In [11]:
dist_by_score <- label %>%
  group_by(score) %>%
  count() %>%
  ggplot(aes(score, n), fill=as.factor(score)) + 
  geom_line() + 
  theme_gray(base_family = "Helvetica") + 
  labs(title="dist_by_score")

dist_by_category <- label %>%
  group_by(category) %>%
  count() %>%
  ggplot(aes(category, n), fill=as.factor(score)) + 
  geom_col() + 
  theme_gray(base_family = "Helvetica") + 
  labs(title="dist_by_category")

layout <- matrix(c(1,2),1,2,byrow=TRUE)
multiplot(dist_by_score, dist_by_category, layout=layout)
In [12]:
label %>% 
    group_by(score) %>%
    count() %>%
    summary()
     score             n        
 Min.   : 1.00   Min.   : 1084  
 1st Qu.: 3.25   1st Qu.: 1546  
 Median : 5.50   Median : 3908  
 Mean   : 5.50   Mean   : 6500  
 3rd Qu.: 7.75   3rd Qu.: 7084  
 Max.   :10.00   Max.   :29559  

* Findings

앞선 카테고리별 length에서 알수 있듯이, 데이터 불균형이 심하다. 객관적 점수에 따른 클래스 분류는 어렵다. 현재 가능한 접근은 아래의 2가지와 같다.

1) 클래스분포가 가장 적은 bad의 데이터 행수에 맞춰서 각 클래스별로 6000개씩만 이용하여 모델 학습
2) 절대적 점수 수치가 아닌, 정규분포 percentile에 맞춰 클래스 분류

2번의 경우, 현재 데이터만으로 전체적 경향성을 표현하기엔 무리가 있지만, 결과의 유의적 변화 정도를 보고 전체 데이터에 대한 분석 방향을 제시하는 근거가 될 수 있다. 그러나, 현재 10점인 데이터가 약 3만행으로 거의 절반 이상을 차지하기 때문에, percentile에 맞춰 3개의 카테고리로 나눌 경우, 'soso'와 'good' 카테고리의 거의 대부분은 10점인 리뷰가 차지하게 되며, good/bad로 이진 분류를 해도 9점 리뷰 일부와 10점 리뷰 대부분이 good 카테고리에 할당된다. 따라서, 현재의 데이터셋으로는 2번 방법은 어렵다고 판단하여 1번 방법으로 분석을 진행한다.


* category별로 6000개씩 샘플링

가장 적은 soso 카테고리에 맞춰 6000개씩 샘플링한다.

In [13]:
set.seed(180419)
bad_sampled <- sample(label[which(label$category=='bad'), ]$id, 6000, replace=FALSE)
soso_sampled <- sample(label[which(label$category=='soso'), ]$id, 6000, replace=FALSE)
good_sampled <- sample(label[which(label$category=='good'), ]$id, 6000, replace=FALSE)
In [14]:
sampled_id <- union(union(bad_sampled, good_sampled), soso_sampled)
In [15]:
dist_by_score <- label %>%
  filter(id %in% sampled_id) %>%
  group_by(score) %>%
  count() %>%
  ggplot(aes(score, n), fill=as.factor(score)) + 
  geom_line() + 
  theme_gray(base_family = "Helvetica") + 
  labs(title="dist_by_score")

dist_by_category <- label %>%
  filter(id %in% sampled_id) %>%
  group_by(category) %>%
  count() %>%
  ggplot(aes(category, n), fill=as.factor(score)) + 
  geom_col() + 
  theme_gray(base_family = "Helvetica") + 
  labs(title="dist_by_category")

layout <- matrix(c(1,2),1,2,byrow=TRUE)
multiplot(dist_by_score, dist_by_category, layout=layout)

비교적 균형 잡힌 분포가 되었다. 실제 모델링에서, 특히 FC layer에서 계층적 구조로 2개의 분류기를 직렬로 연결하는 아키텍쳐를 고려하고 있으므로, 이렇게 카테고리별 샘플링된 데이터셋으로 학습시키도록 한다.


3. Non-semantic 분석

(큰 영향은 없을 것으로 예상되지만) 각 카테고리/점수와 단어의 수/문장 길이가 상관 관계를 가지는지 확인한다.

* 문자열 형태의 각 행을 " "을 기준으로 나누어 {id, token}을 행으로 하는 데이터 프레임으로 변형한다.

In [16]:
cohesion_split <- cohesion %>%
  filter(id %in% sampled_id) %>%
  unnest_tokens(token, review, token="words") %>%
  merge(label, by="id")
In [17]:
mecab_split <- mecab %>%
  filter(id %in% sampled_id) %>%
  unnest_tokens(token, review, token="words") %>%
  merge(label, by="id")

* 각 category에 속하는 token을 따로 저장한다. 단순 빈도로 정렬했을 때, 가장 높은 빈도를 가지는 토큰은 모든 category에 공통으로 속하는 토큰일 확률이 높으므로, 이 3가지 교집합에 속하는 토큰을 제거하는 것이 더 직관적인 결과를 가져온다.

In [18]:
cohesion_token_bad <- cohesion_split %>%
  filter(id %in% bad_sampled) %>%
  distinct(token) %>%
  select(token)

cohesion_token_soso <- cohesion_split %>%
  filter(id %in% soso_sampled) %>%
  distinct(token) %>%
  select(token)

cohesion_token_good <- cohesion_split %>%
  filter(id %in% good_sampled) %>%
  distinct(token) %>%
  select(token)
  
cohesion_token_intersect <- intersect(intersect(cohesion_token_good$token, cohesion_token_bad$token), cohesion_token_soso$token)
#cohesion_token_intersect <- intersect(cohesion_token_good$cohesion_token, cohesion_token_bad$cohesion_token)
In [19]:
mecab_token_bad <- mecab_split %>%
  filter(id %in% bad_sampled) %>%
  distinct(token) %>%
  select(token)

mecab_token_soso <- mecab_split %>%
  filter(id %in% soso_sampled) %>%
  distinct(token) %>%
  select(token)

mecab_token_good <- mecab_split %>%
  filter(id %in% good_sampled) %>%
  distinct(token) %>%
  select(token)
  
mecab_token_intersect <- intersect(intersect(mecab_token_good$token, mecab_token_bad$token), mecab_token_soso$token)
#cohesion_token_intersect <- intersect(cohesion_token_good$cohesion_token, cohesion_token_bad$cohesion_token)
In [44]:
cohesion_bigram <- cohesion %>%
  filter(id %in% sampled_id) %>%
  unnest_tokens(token, review, token="ngrams", n=2) %>%
  merge(label, by="id")

cohesion_bigram_bad <- cohesion_bigram %>%
  filter(id %in% bad_sampled) %>%
  distinct(token) %>%
  select(token)

cohesion_bigram_soso <- cohesion_bigram %>%
  filter(id %in% soso_sampled) %>%
  distinct(token) %>%
  select(token)

cohesion_bigram_good <- cohesion_bigram %>%
  filter(id %in% good_sampled) %>%
  distinct(token) %>%
  select(token)

cohesion_bigram_intersect <- intersect(intersect(cohesion_bigram_good$token, cohesion_bigram_bad$token), cohesion_bigram_soso$token)
#cohesion_bigram_intersect <- intersect(cohesion_bigram_good$token, cohesion_bigram_bad$token)
In [45]:
mecab_bigram <- mecab %>%
  filter(id %in% sampled_id) %>%
  unnest_tokens(token, review, token="ngrams", n=2) %>%
  merge(label, by="id")

mecab_bigram_bad <- mecab_bigram %>%
  filter(id %in% bad_sampled) %>%
  distinct(token) %>%
  select(token)

mecab_bigram_soso <- mecab_bigram %>%
  filter(id %in% soso_sampled) %>%
  distinct(token) %>%
  select(token)

mecab_bigram_good <- mecab_bigram %>%
  filter(id %in% good_sampled) %>%
  distinct(token) %>%
  select(token)

mecab_bigram_intersect <- intersect(intersect(mecab_bigram_good$token, mecab_bigram_bad$token), mecab_bigram_soso$token)
#mecab_bigram_intersect <- intersect(mecab_bigram_good$token, mecab_bigram_bad$token)


In [20]:
category_word_n <- cohesion_split %>%
  count(id) %>% 
  merge(label, by="id") %>%
  group_by(category) %>%
  mutate(word_n_mean = mean(n)) %>%
  select(category, word_n_mean) %>%
  distinct() %>%
  ungroup() %>%
  ggplot(aes(category, word_n_mean), fill=as.factor(category)) + 
  geom_col() + 
  labs(x = NULL, y = "n") +
  labs(title="word_n by category") + 
  theme(legend.position = "none") +
  theme_gray(base_family = "Helvetica") + 
  coord_flip()


category_text_len <- cohesion %>%
  merge(label, by="id") %>%
  mutate(text_len = str_length(review)) %>%
  group_by(category) %>%
  mutate(text_len_mean = mean(text_len)) %>%
  ungroup() %>%
  select(category, text_len_mean) %>%
  distinct() %>%
  ggplot(aes(category, text_len_mean), fill=as.factor(category)) + 
  geom_col() + 
  labs(x = NULL, y = "n") +
  labs(title="text_len by category") + 
  theme(legend.position = "none") +
  theme_gray(base_family = "Helvetica") + 
  coord_flip()
  
score_word_n <- cohesion_split %>%
  count(id) %>% 
  merge(label, by="id") %>%
  group_by(score) %>%
  mutate(word_n_mean = mean(n)) %>%
  select(score, word_n_mean) %>%
  distinct() %>%
  ungroup() %>%
  ggplot(aes(as.factor(score), word_n_mean), fill=as.factor(score)) + 
  geom_col() + 
  labs(x = NULL, y = "n") +
  labs(title="word_n by score") + 
  theme(legend.position = "none") +
  theme_gray(base_family = "Helvetica") + 
  coord_flip()


score_text_len <- cohesion %>%
  merge(label, by="id") %>%
  mutate(text_len = str_length(review)) %>%
  group_by(score) %>%
  mutate(text_len_mean = mean(text_len)) %>%
  ungroup() %>%
  select(score, text_len_mean) %>%
  distinct() %>%
  ggplot(aes(as.factor(score), text_len_mean), fill=as.factor(score)) + 
  geom_col() + 
  labs(x = NULL, y = "n") +
  labs(title="text_len by score") + 
  theme(legend.position = "none") +
  theme_gray(base_family = "Helvetica") + 
  coord_flip()
  
layout <- matrix(c(1,2,3,4), 2,2,byrow=TRUE)
multiplot(category_word_n, category_text_len, score_word_n, score_text_len, layout=layout)
In [21]:
category_word_n <- mecab_split %>%
  count(id) %>% 
  merge(label, by="id") %>%
  group_by(category) %>%
  mutate(word_n_mean = mean(n)) %>%
  select(category, word_n_mean) %>%
  distinct() %>%
  ungroup() %>%
  ggplot(aes(category, word_n_mean), fill=as.factor(category)) + 
  geom_col() + 
  labs(x = NULL, y = "n") +
  labs(title="word_n by category") + 
  theme(legend.position = "none") +
  theme_gray(base_family = "Helvetica") + 
  coord_flip()


category_text_len <- mecab %>%
  merge(label, by="id") %>%
  mutate(text_len = str_length(review)) %>%
  group_by(category) %>%
  mutate(text_len_mean = mean(text_len)) %>%
  ungroup() %>%
  select(category, text_len_mean) %>%
  distinct() %>%
  ggplot(aes(category, text_len_mean), fill=as.factor(category)) + 
  geom_col() + 
  labs(x = NULL, y = "n") +
  labs(title="text_len by category") + 
  theme(legend.position = "none") +
  theme_gray(base_family = "Helvetica") + 
  coord_flip()
  
score_word_n <- mecab_split %>%
  count(id) %>% 
  merge(label, by="id") %>%
  group_by(score) %>%
  mutate(word_n_mean = mean(n)) %>%
  select(score, word_n_mean) %>%
  distinct() %>%
  ungroup() %>%
  ggplot(aes(as.factor(score), word_n_mean), fill=as.factor(score)) + 
  geom_col() + 
  labs(x = NULL, y = "n") +
  labs(title="word_n by score") + 
  theme(legend.position = "none") +
  theme_gray(base_family = "Helvetica") + 
  coord_flip()


score_text_len <- mecab %>%
  merge(label, by="id") %>%
  mutate(text_len = str_length(review)) %>%
  group_by(score) %>%
  mutate(text_len_mean = mean(text_len)) %>%
  ungroup() %>%
  select(score, text_len_mean) %>%
  distinct() %>%
  ggplot(aes(as.factor(score), text_len_mean), fill=as.factor(score)) + 
  geom_col() + 
  labs(x = NULL, y = "n") +
  labs(title="text_len by score") + 
  theme(legend.position = "none") +
  theme_gray(base_family = "Helvetica") + 
  coord_flip()
  
layout <- matrix(c(1,2,3,4), 2,2,byrow=TRUE)
multiplot(category_word_n, category_text_len, score_word_n, score_text_len, layout=layout)

* Findings

딱히 큰 의미는 없어보인다.....



4. Semantic 분석 (token, bigram)

각 카테고리/점수별로 어떤 word* 혹은 bigram이 두드러지는지 확인한다.

4.1 Word 시각화

각 카테고리/점수별로 cohesion과 mecab의 토큰을 시각화한다. cohesion의 경우 (정확히 사전에 등재된 단어는 아니어도) word로 판단할 수 있지만, mecab의 경우는 정확히 morpheme이라고 보는 것이 맞다. 하지만, 본 분석에선 편의를 위해 2가지 모두 word로 표현했다.

In [22]:
options(warn=-1) #한글 폰트로 인해 경고 메세지가 나올 수 있으니 경고를 끄고 시작한다...

2-1-1. cohesion & frequency & category

In [23]:
cohesion_split %>%
  group_by(category) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  select(category, token, token_n) %>%
  filter(!token %in% cohesion_token_intersect) %>%
  group_by(category) %>%
  top_n(20, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(category))) +
  geom_col() + 
  labs(title="token by category") + 
  theme(legend.position = "none") +
  facet_wrap(~ category, ncol=3, scales="free") +
  coord_flip()

2-1-2. cohesion & frequency & score

In [24]:
cohesion_split %>%
  group_by(score) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  select(score, token, token_n) %>%
  filter(!token %in% cohesion_token_intersect) %>%
  group_by(score) %>%
  top_n(5, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(score))) +
  geom_col() + 
  labs(title="token by score") + 
  theme(legend.position = "nonㅠe") +
  facet_wrap(~ score, ncol=3, scales="free") +
  coord_flip()

* Findings

결과는 굉장히 납득할만하며, 무엇보다 cohesion_tokenizer의 장점이 드러난다. 2-1-1번의 차트에서 '꿀잼', '노잼' 등의 단어는 보통 semantic informative한 단어인데, 사전에 등록되어있지 않은 경우가 많다. 여기서도 '꿀잼'과 '노잼'은 빈도수가 40 이상으로 해당 카테고리를 아주 잘 대변하는 토큰인데, 다른 형태소 분석기가 사전에 등록되지 않은 단어에 대해서 취약한 반면, cohesion_tokenizer는 등록되지 않은 단어도 토큰화했다. 단점은 '지루하지', '지루했음'처럼 같은 어근을 가진 단어도 다른 토큰으로 나뉘면서 생기는 정보의 중복 문제가 있다. 또, 전체적으로 빈도수가 낮은 문제가 있는데, 각 카테고리별로 6000개씩 샘플링했음에도 good 카테고리의 경우 상위 20개 단어의 빈도수 합은 크게 쳐도 1000이 안된다. 즉 나머지 5000개의 리뷰를 판단할 근거가 부족하다는 뜻이다. (2-1-2번 차트에서는 두드러지게 낮은 빈도수로 나타난다.) 이 부분은 다른 형태소분석기(이번 분석에서 사용할 mecab과 같은)에서도 나타날 수 있으므로 mecab과 함께 비교해본다.


2-1-3. mecab & frequency & category

In [25]:
mecab_split %>%
  group_by(category) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  select(category, token, token_n) %>%
  filter(!token %in% mecab_token_intersect) %>%
  group_by(category) %>%
  top_n(20, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(category))) +
  geom_col() + 
  labs(title="token by category") + 
  theme(legend.position = "none") +
  facet_wrap(~ category, ncol=3, scales="free") +
  coord_flip()

2-1-4. mecab & frequency & score

In [26]:
mecab_split %>%
  group_by(score) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  select(score, token, token_n) %>%
  filter(!token %in% mecab_token_intersect) %>%
  group_by(score) %>%
  top_n(5, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(score))) +
  geom_col() + 
  labs(title="token by score") + 
  theme(legend.position = "nonㅠe") +
  facet_wrap(~ score, ncol=3, scales="free") +
  coord_flip()

* Findings

역시 결과는 굉장히 납득할만하며, mecab은 cohesion과 같이 사전에 등록되지 않은 단어는 반환하지 못했지만, 사전에 등재된 단어들을 잘 나타냈다. cohesion의 경우 같은 어근(의미)를 가진 단어가 다른 어미와 결합되어 높은 엔트로피를 보이는 경우 정보량이 높은 토큰을 누락할 수 있다. 즉 "재밌게"와 "재미 있는"가 전체 리뷰의 good 카테고리에서 각각 5번, 5번 정도 나타나는 경우 top_n 기준에 따라 둘 다 탈락시킬 수 있다. 하지만, mecab의 경우, "재밌게"와 "재미 있는"에서 모두 "재미"를 추출해 총 10번의 빈도수를 보이므로 "재미"라는 유의미한 단어가 누락되지 않게 할 수 있다. 위 2-1-3번에서 mecab의 그러한 장점이 잘 드러난다. mecab의 경우도 앞선 cohesion과 동일하게 출현하는 토큰의 빈도수가 전체 데이터셋 크기에 비해 적은 문제가 발생한다. 또, cohesion과 mecab 모두 2-1-2번의 7점대 차트와 2-1-4번의 7점대 차트처럼 공통으로 중복된 빈도수를 보이는 토큰이 많은 카테고리 혹은 점수 때문에 input data의 sequence가 가변적 길이를 가지게 되는 문제가 생기는데, 이번에 사용할 CNN 모델은 FC layer 전에 max pooling을 이용해 가변적 시퀀스도 문제 없이 처리할 수 있다.


2-1-5. cohesion & tf-idf & category

In [28]:
#tf-idf 상위 값의 토큰
cohesion_split %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  group_by(category) %>%
  top_n(20, tf_idf) %>%
  ungroup() %>%
  ggplot(aes(reorder(token, tf_idf), tf_idf, fill=as.factor(category))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ category, ncol=3, scales="free") +
  coord_flip()
In [30]:
#평균 tf-idf 값 이상의 토큰을 n 내림차순으로 정렬
cohesion_split %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  mutate(mean_tfidf = mean(tf_idf)) %>%
  filter(tf_idf >= mean(tf_idf)) %>%
  group_by(category) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  mutate(token_len = str_length(token)) %>%
  select(category, token, token_n, token_len) %>%
  filter(token_len > 1) %>%
  filter(!token %in% mecab_token_intersect) %>%
  group_by(category) %>%
  top_n(20, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(category))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ category, ncol=3, scales="free") +
  coord_flip()

2-1-6. cohesion & tf-idf & score

In [31]:
#tf-idf 상위 값의 토큰
cohesion_split %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  group_by(score) %>%
  top_n(5, tf_idf) %>%
  ungroup() %>%
  ggplot(aes(reorder(token, tf_idf), tf_idf, fill=as.factor(score))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ score, ncol=3, scales="free") +
  coord_flip()
In [33]:
#평균 tf-idf 값 이상의 토큰을 n 내림차순으로 정렬
cohesion_split %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  mutate(mean_tfidf = mean(tf_idf)) %>%
  filter(tf_idf >= mean(tf_idf)) %>%
  group_by(score) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  mutate(token_len = str_length(token)) %>%
  select(score, token, token_n, token_len) %>%
  filter(token_len > 1) %>%
  filter(!token %in% cohesion_token_intersect) %>%
  group_by(score) %>%
  top_n(5, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(score))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ score, ncol=3, scales="free") +
  coord_flip()

* Findings

왠만 려다 등등 의미가 없어보이는 단어는 전체적으로 사라진 걸 확인할 수 있다. 2-1-5번 ~ 2-1-6번의 위 차트에서 tf-idf를 사용한 시각화에서 전반적으로 확인할 수 있듯이, tf-idf만 이용한 결과는 토큰의 semantic은 각 카테고리/점수에 맞게 분포되어있지만, idf로 인한 페널티가 많이 반영되어서인지 일반적으로 볼 수 있는 토큰이 아닌 단어들보다 의미가 강한대신 쉽게 보기 힘든 (말 그대로 '의미' 자체의 정보량만 높은) 토큰들이 분포되어있는 것을 확인할 수 있다. 이런 토큰들로 모델을 학습시킬 경우 해당 토큰을 포함하는 리뷰에 대해선 비교적 정확히 예측을 하겠지만, 그렇지 않은 리뷰는 예측할 수 없는 모델이 된다. 즉, 모델의 범용적 표현력이 떨어진다고 할 수 있다.

반면에 tf-idf 평균값 이상을 가진 토큰들을 빈도수로 다시 뽑은 결과는 2-1-1번 ~ 2-1-2번에서의 빈도수만을 이용해 추출한 정보량 높은 토큰을 더 잘 추출하며 토큰들도 비교적 범용적인 것을 확인할 수 있다. 2-1-1번의 'bad' 카테고리에 있는 '왠만', '려다'와 같은 토큰은 사실 그 자체로는 어떤 semantic을 가지고 있는지 예측하기 어려운데, 2-1-5번과 2-1-6번의 아래 차트에선 그런 단어들이 사라진 것을 확인할 수 있다. 또, 중복 값으로 인해 너무 많은 토큰이 점수에 할당되는 문제도 어느 정도 완화된 것을 확인할 수 있다.


2-1-7. mecab & tf-idf & category

In [34]:
#tf-idf 상위 값의 토큰
mecab_split %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  group_by(category) %>%
  top_n(20, tf_idf) %>%
  ungroup() %>%
  ggplot(aes(reorder(token, tf_idf), tf_idf, fill=as.factor(category))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ category, ncol=3, scales="free") +
  coord_flip()
In [35]:
#평균 tf-idf 값 이상의 토큰을 n 내림차순으로 정렬
mecab_split %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  mutate(mean_tfidf = mean(tf_idf)) %>%
  filter(tf_idf >= mean(tf_idf)) %>%
  group_by(category) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  mutate(token_len = str_length(token)) %>%
  select(category, token, token_n, token_len) %>%
  filter(token_len > 1) %>%
  filter(!token %in% mecab_token_intersect) %>%
  group_by(category) %>%
  top_n(20, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(category))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ category, ncol=3, scales="free") +
  coord_flip()

2-1-8. mecab & tf-idf & score

In [36]:
#tf-idf 상위 값의 토큰
mecab_split %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  group_by(score) %>%
  top_n(5, tf_idf) %>%
  ungroup() %>%
  ggplot(aes(reorder(token, tf_idf), tf_idf, fill=as.factor(score))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ score, ncol=3, scales="free") +
  coord_flip()
In [37]:
#평균 tf-idf 값 이상의 토큰을 n 내림차순으로 정렬
mecab_split %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  mutate(mean_tfidf = mean(tf_idf)) %>%
  filter(tf_idf >= mean(tf_idf)) %>%
  group_by(score) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  mutate(token_len = str_length(token)) %>%
  select(score, token, token_n, token_len) %>%
  filter(token_len > 1) %>%
  filter(!token %in% mecab_token_intersect) %>%
  group_by(score) %>%
  top_n(5, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(score))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ score, ncol=3, scales="free") +
  coord_flip()

* Findings

cohesion & tf-idf와 유사한 결과를 보인다. mecab 토큰 역시 tf-idf만 사용하는 경우보다 tf-idf 후 frequency로 정렬하는 것이 더 납득할만한 결과를 보이고 있다. 하지만, cohesion과 mecab의 토큰화 방식 차이에서 오는 input data 정보의 특성(cohesion - Out of Vocabulary에 더 강하며, 사전에 없는 단어라도 정보량이 높다면 포함, mecab - 토큰화를 통해 다른 어미가 결합해있어도 같은 어근으로 분류)의 차이는 여전히 존재했다.



4.2 bigram 시각화

각 카테고리/점수별로 cohesion과 mecab의 bigram 토큰을 시각화한다. bigram의 경우, 각 방법에 따른 결과의 차이는 앞선 word 시각화와 거의 유사하다.

2-2-1. cohesion & frequency & category

In [46]:
cohesion_bigram %>%
  group_by(category) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  mutate(text_len = str_length(token)) %>%
  select(category, token, token_n, text_len) %>%
  filter(!token %in% cohesion_bigram_intersect) %>%
  filter(text_len > 3) %>%
  group_by(category) %>%
  top_n(20, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(category))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ category, ncol=3, scales="free") +
  coord_flip()

2-2-2. cohesion & frequency & score

In [47]:
cohesion_bigram %>%
  group_by(score) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  mutate(text_len = str_length(token)) %>%
  select(category, token, token_n, text_len) %>%
  filter(!token %in% cohesion_bigram_intersect) %>%
  filter(text_len > 3) %>%  group_by(score) %>%
  top_n(5, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(score))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ score, ncol=3, scales="free") +
  coord_flip()

2-2-3. mecab & frequency & category

In [57]:
mecab_bigram %>%
  group_by(category) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  mutate(text_len = str_length(token)) %>%
  select(category, token, token_n, text_len) %>%
  filter(!token %in% mecab_bigram_intersect) %>%
  filter(text_len > 3) %>%
  group_by(category) %>%
  top_n(20, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(category))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ category, ncol=3, scales="free") +
  coord_flip()

2-2-4. mecab & frequency & score

In [59]:
mecab_bigram %>%
  group_by(score) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  mutate(text_len = str_length(token)) %>%
  select(score, token, token_n, text_len) %>%
  filter(!token %in% mecab_bigram_intersect) %>%
  filter(text_len > 3) %>%
  group_by(score) %>%
  top_n(5, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(score))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ score, ncol=3, scales="free") +
  coord_flip()

2-2-5. cohesion & tf-idf & category

In [66]:
#tf-idf 상위 값의 토큰
cohesion_bigram %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  group_by(category) %>%
  top_n(20, tf_idf) %>%
  ungroup() %>%
  ggplot(aes(reorder(token, tf_idf), tf_idf, fill=as.factor(category))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ category, ncol=3, scales="free") +
  coord_flip()
In [67]:
#평균 tf-idf 값 이상의 토큰을 n 내림차순으로 정렬
cohesion_bigram %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  mutate(mean_tfidf = mean(tf_idf)) %>%
  filter(tf_idf >= mean(tf_idf)) %>%
  group_by(category) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  mutate(token_len = str_length(token)) %>%
  select(category, token, token_n, token_len) %>%
  filter(token_len > 3) %>%
  filter(!token %in% cohesion_bigram_intersect) %>%
  group_by(category) %>%
  top_n(20, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(category))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ category, ncol=3, scales="free") +
  coord_flip()

2-2-6. cohesion & tf-idf & score

In [68]:
#tf-idf 상위 값의 토큰
cohesion_bigram %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  group_by(score) %>%
  top_n(5, tf_idf) %>%
  ungroup() %>%
  ggplot(aes(reorder(token, tf_idf), tf_idf, fill=as.factor(score))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ score, ncol=3, scales="free") +
  coord_flip()
In [69]:
#평균 tf-idf 값 이상의 토큰을 n 내림차순으로 정렬
cohesion_bigram %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  mutate(mean_tfidf = mean(tf_idf)) %>%
  filter(tf_idf >= mean(tf_idf)) %>%
  group_by(score) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  mutate(token_len = str_length(token)) %>%
  select(score, token, token_n, token_len) %>%
  filter(token_len > 3) %>%
  filter(!token %in% cohesion_bigram_intersect) %>%
  group_by(score) %>%
  top_n(5, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(score))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ score, ncol=3, scales="free") +
  coord_flip()

2-2-7. mecab & tf-idf & category

In [75]:
#tf-idf 상위 값의 토큰
mecab_bigram %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  group_by(category) %>%
  top_n(20, tf_idf) %>%
  ungroup() %>%
  ggplot(aes(reorder(token, tf_idf), tf_idf, fill=as.factor(category))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ category, ncol=3, scales="free") +
  coord_flip()
In [76]:
#평균 tf-idf 값 이상의 토큰을 n 내림차순으로 정렬
mecab_bigram %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  mutate(mean_tfidf = mean(tf_idf)) %>%
  filter(tf_idf >= mean(tf_idf)) %>%
  group_by(category) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  mutate(token_len = str_length(token)) %>%
  select(category, token, token_n, token_len) %>%
  filter(token_len > 3) %>%
  filter(!token %in% mecab_bigram_intersect) %>%
  group_by(category) %>%
  top_n(20, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(category))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ category, ncol=3, scales="free") +
  coord_flip()

2-2-8. mecab & tf-idf & score

In [74]:
#tf-idf 상위 값의 토큰
mecab_bigram %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  group_by(score) %>%
  top_n(5, tf_idf) %>%
  ungroup() %>%
  ggplot(aes(reorder(token, tf_idf), tf_idf, fill=as.factor(score))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ score, ncol=3, scales="free") +
  coord_flip()
In [73]:
#평균 tf-idf 값 이상의 토큰을 n 내림차순으로 정렬
mecab_bigram %>%
  count(id, token) %>%
  bind_tf_idf(token, id, n) %>%
  select(id, token, tf_idf) %>%
  merge(label, by='id') %>%
  mutate(mean_tfidf = mean(tf_idf)) %>%
  filter(tf_idf >= mean(tf_idf)) %>%
  group_by(score) %>%
  count(token) %>%
  ungroup() %>%
  mutate(token_n = n) %>%
  mutate(token_len = str_length(token)) %>%
  select(score, token, token_n, token_len) %>%
  filter(token_len > 3) %>%
  filter(!token %in% mecab_bigram_intersect) %>%
  group_by(score) %>%
  top_n(5, token_n) %>%
  ungroup() %>%
  ggplot(aes(token, token_n, fill=as.factor(score))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ score, ncol=3, scales="free") +
  coord_flip()

* Findings

bigram 분석에서 2개의 단어가 연달아 나옴으로써 가질 수 있는 semantic이 반영된 것을 볼 수 있다. 이전 word 시각화에선 '재미'라는 단어만으론 카테고리를 예측하기 어려웠는데, bigram에서는 "별로 재미", "재미 있습니다", "지만 재미"와 같이 어느 정도 맥락적 의미를 담은 토큰을 확인할 수 있다. word와 마찬가지로 bigram도 유의미한 토큰임을 확인할 수 있다.


4.3. 품사 시각화

사실 해당 데이터셋에 대해 tag 분석이 큰 의미가 있을 거라 예상하진 않는다. 만약 데이터셋이 다양한 장르의 텍스트가 합쳐진 경우라면 의미가 있겠지만, 리뷰들로 이루어진 본 데이터셋에서는 크게 의미가 있을 것으로 보이지 않는다. 예를 들어, 비문학과 문학 텍스트가 혼합된 데이터셋이라면 문학 텍스트에선 상대적으로 용언이 많이 나타나고 비문학 텍스트에서는 문학 텍스트에 비해 체언이 많이 나타날 것이라고 예상할 수 있다. 그러므로, 아래 차트는 같은 장르의 텍스트 내에서라면 tag 분석이 큰 의미가 없음을 확인하는 의미에서 포함시켰다.

In [77]:
tag_split <- tag %>% 
  filter(id %in% sampled_id) %>%
  unnest_tokens(tag, tag, token="words") %>%
  mutate(tag = toupper(tag)) %>%
  merge(label, by="id")

tag_bad <- tag_split %>%
  filter(id %in% bad_sampled) %>%
  distinct(tag) %>%
  select(tag)

tag_soso <- tag_split %>%
  filter(id %in% soso_sampled) %>%
  distinct(tag) %>%
  select(tag)

tag_good <- tag_split %>%
  filter(id %in% good_sampled) %>%
  distinct(tag) %>%
  select(tag)

tag_intersect <- intersect(intersect(tag_good$tag, tag_bad$tag), tag_soso$tag)
#tag_intersect <- intersect(tag_good$tag, tag_bad$tag)
In [82]:
tag_split %>%  
  group_by(category) %>%
  count(tag) %>%
  ungroup() %>%
  mutate(tag_n = n) %>%
  select(category, tag, tag_n) %>%
  group_by(category) %>%
  top_n(20, tag_n) %>%
  ungroup() %>%
  ggplot(aes(reorder(tag, tag_n), tag_n, fill=as.factor(category))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ category, ncol=3, scales="free") +
  coord_flip()
In [84]:
tag_split %>%  
  group_by(score) %>%
  count(tag) %>%
  ungroup() %>%
  mutate(tag_n = n) %>%
  select(score, tag, tag_n) %>%
  group_by(score) %>%
  top_n(10, tag_n) %>%
  ungroup() %>%
  ggplot(aes(reorder(tag, tag_n), tag_n, fill=as.factor(score))) +
  geom_col() + 
  labs(x = NULL, y = "n") +
  theme(legend.position = "none") +
  facet_wrap(~ score, ncol=3, scales="free") +
  coord_flip()

* Findings

예상대로 크게 의미 있는 결과가 보이진 않는다.



위의 시각화를 통해

  • cohesion과 mecab은 모두 tokenizer지만, cohesion은 Out of Vocabulary에 더 강점을 보이며 사전에 없는 단어라도 정보량이 높다면 포함시키고, mecab은 다른 어미가 결합해있어도 같은 어근으로 분류한다는 장점이 있다.
  • word와 bigram의 의미 모두 납득할만하다. (이번 CNN 모델의 경우 filter size에 따라 모두 포함할 수 있는 정보이다.)
  • tf-idf & 빈도수를 섞어쓰는 방법이 제일 괜찮아보인다.

cohesion과 mecab은 각각 다른 특성의 토큰을 추출하는데 특화되어 있다. 둘 중 한가지 토큰화만 사용할 경우 다른 토큰화 방식의 장점은 살리지 못하기 때문에 둘을 혼합할 필요가 있다.

A) 2개의 CNN 모델을 만들어 앙상블 기법을 사용한다.
B) 채널에 두 가지 방법으로 추출한 토큰을 모두 담는다.

A번의 경우 시도해볼 수 있지만 비교적 컴퓨팅 문제가 있고, B번의 경우 두 방법을 한 레이어에 혼합해 사용하므로 bigram이나 trigram으로 인한 정보는 구성하기 어렵고, 차원이 지나치게 커지는 문제가 발생한다. 현재 레퍼런스로 삼는 CNN 기본 모델의 경우 filter 사이즈를 다르게 함으로써 bigram, trigram 등의 정보를 반영하는데, cohesion과 mecab 토큰을 배열하는 방법에 따라 그 정보를 잃어버릴 가능성이 있다. 또, 2 방법의 토큰을 모두 한 레이어에 넣게 되면 차원이 2배로 커지게 되는데, 연산 비용은 그에 따라 지수적으로 증가하므로 역시 컴퓨팅 비용이 비효율적이게 된다.

이미지 처리에 있어서 CNN은 흑백 이미지의 경우 1가지 채널을 가진 2차원 NM 행렬로, RGB 이미지의 경우 3가지 채널을 가진 2차원 NM 행렬(즉, 3NM차원의 큐브)로 표현한다. 이 큐브의 각 채널은 색을 표현하는 방법만 다르고 전체적인 맥락은 유사한데, 이 방법을 응용한 모델을 설계한다. 논문의 multi-channel layer의 경우 fine-tunning을 위한 것이므로 조금 다르지만, 전체적인 모델의 설계도는 유사하다. cohesion token으로 이루어진 layer와 mecab token으로 이루어진 layer, 2개의 채널을 갖는데, 각각의 채널은 포함하고 있는 정보의 맥락은 유사하지만 특성이 다르므로 시도해볼만한 가치가 있다.

또, 시각화의 목표였던 3가지에 대해서도 어느 정도 인사이트를 얻을 수 있었다.

1) FC layer에 2개의 분류기를 직렬로 연결하는 계층적 구조의 CNN 모델이 의미가 있는지
-> 카테고리별로 word/bigram 토큰이 다르게 분포되어있는 걸 확인했다. 각 카테고리별로 유의미한 분류를 하는 token으로 분류를 하고, 각 점수를 대표하는 token으로 각각 다시 분류하는 것은 시도해볼만한 가치가 있다.
(FC layer에서 분류기 2개를 직렬로 연결한 연구 사례는 [Land use Classification using Convolutional Neural Networks Appled to Ground-Level Images] 논문을 참고했습니다.)

2) input data를 어떻게 손질할지와 각 방법은 어떤 차이를 보이는지 (cohesion vs. mecab, word vs. bigram)
-> word와 bigram의 semantic은 따로 전처리하지 않아도 현재 CNN 모델에서 filter size를 이용해 반영한다. cohesion vs. mecab은 multi-channel 구조를 통해 모델이 각 tokenizer의 특성을 모두 반영할 수 있도록 한다.
(기본 CNN 모델의 구조와 multi-channel에 구조는 [Convolutional Neural Networks for Sentence Classification] 논문을 참고했습니다.)

3) 정보량이 높은 token(혹은 bigram)을 어떻게 추출할지
-> tf-idf가 평균값 이상인 token만 이용한다. 본 시각화에선 편의상 tf-idf 필터링 이후 빈도수에 따라 내림차순으로 정렬했지만, CNN 모델은 max pooling을 이용해 가변적 시퀀스도 처리할 수 있으므로, 다시 n으로 정렬하는 과정을 불필요할 것으로 예상된다.